查看原文
其他

实战FreeRTOS的UsageFault异常

格蠹老雷 格友 2023-06-10
在即将过去的2022年里,我花了很多时间在ARM-M核上。M核的最大优点就是小巧轻便,整个系统不如信用卡大,重量还没有一枚硬币重。

能方面,它也有很多玩法,闪存、USB、GPIO,CoreSight技术等等,都值得探寻。

当然,如果给它加上操作系统,就更有意思了。

ARM-M核是典型的MCU,它上面的操作系统有个统一的称呼,一般称为RTOS(实时操作系统)。

在众多的RTOS中,FreeRTOS是流行度很高的一种。在

sourceforge的2022 RTOS排名中(Compare the Top Real-Time Operating Systems (RTOS) of 2022)位居首位。

前些天,我着手把FreeRTOS移植到包含ARM-M核的GDK3上。最初我使用的是沁恒移植的版本,因为GDK3的SoC是沁恒的CH32F103。但是在编译汇编代码时遇到问题,沁恒版本使用的是Keil编译工具,而我不想依赖Keil,想完全基于GCC的开源工具链。
既然这样,我只好到官方项目中找汇编部分的代码。FreeRTOS的模块化工作做的很好,把硬件和编译器差异部分单独放在portable子目录下。
替换了GCC版本的汇编部分代码后,编译通过。但是明显功能有问题。多线程没有跑起来。
使用挥码枪观察中断向量表(IVT),很快发现了故障。原因是port.c中的中断处理函数入口名字和标准的不一样。这导致处理异常时,执行的是空的桩函数,直接进死循环了。比如,执行SVC指令时,直接进了下面这个循环。
u lk!EXTI2_IRQHandler
lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 78]:
8001c50 e7fe b #0x8001c50

比较其它版本的代码,发现有些版本中有如下宏定义:

/* Corresponding to startup.s  */

#define vPortSVCHandler        SVC_Handler

#define xPortPendSVHandler   PendSV_Handler

#define xPortSysTickHandler  SysTick_Handler

上面的宏是把代码里的函数名重定义为标准的名字。按一般的C语言习惯,这种用法是有点古怪的。但是实际效果是有效的。

加上这几个宏,再编译更新,使用NDB的dds命令观察IVT,可以看到上面三个宏相关的中断处理函数都换成新的了。

dds 0
00000000 20005000
00000004 08001c0d lk!Reset_Handler+0x1 [../../startup/startup_GDK3.s @ 39]
00000008 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
0000000c 08001b7d lk!HardFault_Handler+0x1 [../../startup/exceptions.c @ 279]
00000010 08001ba1 lk!MemManage_Handler+0x1 [../../startup/exceptions.c @ 294]
00000014 08001bc5 lk!BusFault_Handler+0x1 [../../startup/exceptions.c @ 309]
00000018 08001be9 lk!UsageFault_Handler+0x1 [../../startup/exceptions.c @ 324]
0000001c 00000000
00000020 00000000
00000024 00000000
00000028 00000000
0000002c 08001441 lk!SVC_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 229]
00000030 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
00000034 00000000
00000038 08001551 lk!PendSV_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 404]
0000003c 080015ad lk!SysTick_Handler+0x1 [portable/RVDS/ARM_CM3/port.c @ 441]
00000040 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]

更重要的是,有了这个修改后,两个示例线程开始跑了,在调试器里可以看到线程工作函数开头的printf函数打印的信息。

gdk3:SystemClk:72000000

gdk3:FreeRTOS Kernel Version:V10.4.4+

gdk3:task1 entry

gdk3:task2 entry

可是,进展就到这里,接下来就出异常了。

gdk3:**** EXCEPTION OCCURRED CFSR=0x20000****

gdk3:Type: Usage Fault

gdk3:Reason: invalid EPSR

gdk3:

gdk3:R0=0 R1=2000108c

gdk3:R2=10000000 R3=e000ed04

gdk3:R12=8002c5f LR=8000e87

gdk3:PC=2000001c PSR=0

gdk3:HFSR=deadd0d0 CFSR=20000


值得说明的是,FreeRTOS在异常处理方面是比较弱的,一旦出问题,一般的做法就是跳到一个死循环,根本没有NT那样的蓝屏,与Linux的Panic机制相比,也要弱很多。

大家现在看到的打印信息还是我后来加进去的。

上面错误信息的根据是硬件的CFSR寄存器,这个寄存器把M的异常分为三大类:UsageFault,BusFault和MemManage,即用法错、总线错和内存管理(错)。

那么,什么是用法错呢?简单说,就是”软件把硬件用错了“。是站在硬件的立场来说的:”你们这帮程序员啊,不学无术,乱写代码,这么好的硬件用不好,不按规则做,到处犯规...读书时不认真学习,毕业了不勤奋实践,真是拿你们没有办法^_^“

对于”用法错“,M核的手册上又细分为多种,每个比特位代表一种。我们遇到的是所谓为INVSTATE,即无效状态位。

简单来说,ARM的指令分为四字节的普通指令,基于2或4字节的THUMB指令。为了区分这两种指令,便搞出了一系列规矩,比如程序状态寄存器(PSR)里有个T位(THUMB),T位为1时,代表要执行的是THUMB指令,T位为0时代表要执行的是标准。

对于GDK3使用的M核来说,它只支持THUMB指令。因此T位应该总是为1,如果谁将其改写为0,那么就是捣乱,就是闹事,就是故意和硬件团队过不去。得到的后果就是UsageFault,准确的说是UsageFault.INVSTATE。

使用ARM文档的话,这个错误的原因是:

Instruction executed with invalid EPSR.T or EPSR.IT field.

上面的信息主要来自ARM的ARM(架构参考手册)。根据多年的经验,我把这个异常的理论原因搞得比较清楚了。但是,这与实际解决问题还有很大的距离。

接下来需要寻找是哪里的代码要把EPSR的T位清零。要知道像EPSR这样的寄存器,很多位是不可以直接读写的,是处理器自动维护的。

用ARM的ARM里的话来说:

The EPSR fields are read-only. The processor ignores any attempt by privileged software to write to them.

看到这里,软件工程师可能有话要说了:”既然是硬件自动修改T位,那凭啥还报软件用法错?“
硬件工程师的回答是:”可能是你们软件间接导致的啊?“
比如加载PC寄存器时,如果目标地址的低位为1,那么硬件就会设置T位,如果为0,那么就会清除T位。因为这个原因,IVT表里的处理函数地址都是实际函数地址加1。
00000004 08001c0d lk!Reset_Handler+0x1 [../../startup/startup_GDK3.s @ 39]
00000008 08001c51 lk!EXTI2_IRQHandler+0x1 [../../startup/startup_GDK3.s @ 78]
0000000c 08001b7d lk!HardFault_Handler+0x1 [../../startup/exceptions.c @ 279]
00000010 08001ba1 lk!MemManage_Handler+0x1 [../../startup/exceptions.c @ 294]
00000014 08001bc5 lk!BusFault_Handler+0x1 [../../startup/exceptions.c @ 309]
00000018 08001be9 lk!UsageFault_Handler+0x1 [../../startup/exceptions.c @ 324]
本来的函数地址都是偶数,加上1后,变为奇数,也就是bit 0为1。
正当分析到这里的时候,新冠病毒来了,我只好休息几天。并把这个问题和好朋友杨文波说了,代码也发了一份给他。
昨天中午时,微信嘟嘟嘟连续跳出一串消息,都是来自文波,我看了前面几条后,顿时兴奋,给他回复了一个大拇指。


最关键的是第一条消息:
得在函数定义的地方这样加 naked attribute, 不然就不是真的naked 函数

naked是什么?凡是学点英文的都知道。没学过英文甚至也知道。

但是对于很多程序员来说,虽然知道naked的字面意思,但却可能不了解它的用法。因为这个编程知识真是用的不多。

长话短说,普通的函数都不是naked,都是穿着衣服的。所谓衣服就是函数开头和结尾的包装,一般称为函数的序言和结语,是编译器自动加上的。

而所谓的naked函数,就是不带包装的,没穿衣服的。

函数序言和结语都做什么呢?主要是操作栈。也就是在开头把要保存的数据压进栈,在末尾时再弹出。分配和释放局部变量一般也是这时做的。

对于普通的函数,编译器都会给他们加上包装。而对于异常处理函数来说,CPU在处理异常时,直接飞进这些函数,这时要求精确的操作栈,这也是为什么要用汇编语言的原因。

那么为什么漏了naked属性呢?

准确地说,不是漏,而是误会了。因为naked属性并不常用,它的写法不是C语言的标准,所以不同编译器的写法不同。

以本例来说,本来使用的代码是这样写的:

void xPortPendSVHandler( void ) __attribute__( ( naked ) );

void xPortSysTickHandler( void );

void vPortSVCHandler( void ) __attribute__( ( naked ) );

也就是把__attribute__( ( naked ) )放在函数原型声明语句里,而且是放在函数名后面。

但是实际情况证明这种写法无效,产生的目标代码是假的nake,编译器还会加上栈操作。比如:

u lk!PendSV_Handler
lk!PendSV_Handler [portable/RVDS/ARM_CM3/port.c @ 404]:
8001550 b480 push {r7}
8001552 af00 add r7, sp, #0

而真正的第一条语句应该是mrs。

void xPortPendSVHandler( void ){ /* This is a naked function. */

__asm volatile ( " mrs r0, psp \n" " isb \n" " \n"


对于GCC编译器,正确的写法是在函数开头这样写:

__attribute__( ( naked ) ) void xPortPendSVHandler( void ){ /* This is a naked function. */ __asm volatile

这样纠正写法后,一切就都正常了!

我与文波是在2016年12月的庐山研习班上认识的。查找相册,刚好找到当年在五老峰前的一张合影(后排右一为文波)。


时光匆匆,转眼6年过去了。我和文波如今是经常聊天的好朋友,在今年下半年的IOT课程上,我们一起合作隔周一次的试验部分。这次在我新冠阳了的时候,他侠客一般果断出手,帮我解决了一个大bug,可谓雪中送炭。

2022年就要过去了,对于所有人来说,这都是非常难忘的一年。困难终将过去,未来无限美好,在此衷心祝愿格友们新年快乐,2023年里天天进步。


(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************


正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

也欢迎关注格友公众号


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存